/**
 * @file generator.c
 * @brief The foundation for the workload generator.
 */
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <math.h>
#include "cJSON.h"
#include "generator.h"
#include "random.h"
#include "resource_model.h"
#include "cpu.h"
#include "cache.h"
#include "memory.h"
#include "disk.h"

typedef struct workgen_list workgen_list;

struct workgen_list {
  workgen_list *next;
  char *resource_label;
  resource_model *(*init)(cJSON *);
};

static workgen_list workgens[HASHSIZE];

static thread_list *threads;

static int CNT_RESOURCES;

/** A counter that keeps track of how many resource threads are ready to begin generating workloads. */
int threads_ready = 0;

/** A mutex to control access to the threads_ready counter. */
pthread_mutex_t threads_ready_mutex = PTHREAD_MUTEX_INITIALIZER;

/** The condition on which all resource threads wait on to begin. */
pthread_cond_t threads_start = PTHREAD_COND_INITIALIZER;

pthread_mutex_t generators = PTHREAD_MUTEX_INITIALIZER;

void setup_generator() {
  printf("Setting up the Workload Generator.\n");
  setup_random_environment();
  threads = NULL;
  install_resource("cpu",setup_cpu);
  install_resource("disk",setup_disk);
  install_resource("memory",setup_memory);
  install_resource("l1_cache",setup_l1_cache);
  install_resource("l2_cache",setup_l2_cache);
}

int add_thread(pthread_t **thread) {
  *thread = malloc(sizeof(pthread_t));
  thread_list *list = malloc(sizeof(thread_list));
  list->next = threads;
  list->thread = *thread;
  threads = list;
  return 0;
}

void rendezvous(pthread_cond_t *start, pthread_mutex_t *mutex, int *counter, int thr_count, char *message) {
  pthread_mutex_lock(mutex);
  if(++*counter == thr_count) {
    printf("%s\n",message);
    pthread_cond_broadcast(start);
  }
  else {
    pthread_cond_wait(start,mutex);
  }
  pthread_mutex_unlock(mutex);
}

cJSON * parse_config(char *path) {
  FILE *f= fopen(path,"rb");
  fseek(f,0,SEEK_END);
  long len=ftell(f);
  fseek(f,0,SEEK_SET);
  char *data=malloc(len+1);
  fread(data,1,len,f);
  fclose(f);
  cJSON *config = cJSON_Parse(data);
  if (!config || config->type != cJSON_Array) {
    perror("Config file is either not valid JSON or not an array.\n"); exit(2);
  }
  free(data);
  return config;
}

/**
   Given an array of JSON Objects, spawn the appropriate generators.
 */
int create_workgen(cJSON *config) {
  if(config->type != cJSON_Array) {
    perror("Config File is not a JSON Array\n");
    return -1;
  }
  CNT_RESOURCES = cJSON_GetArraySize(config);
  cJSON *c = config->child;
  for(c; c; c = c->next) {
    if(create_resource(c)) {
      perror("Invalid Resource Given\n");
      exit(2);
    }
  }
  return 0;
}

int install_resource(char *resource_label, resource_model *(*init)(cJSON *)) {
  hashed target = hash(resource_label);
  workgen_list *new = malloc(sizeof(workgen_list));
  new->next = &workgens[target];
  new->resource_label = resource_label;
  new->init = init;
  workgens[target] = *new;
  return 0;
}

thread_list *get_threads() {
  return threads;
}

int create_resource(cJSON *resource) {
  cJSON *resource_name = cJSON_GetObjectItem(resource,"resource");
  if(!resource_name && resource_name->valuestring) {
    return -1;
  }
  hashed target = hash(resource_name->valuestring);
  workgen_list *tmp;
  for(tmp = &workgens[target]; tmp; tmp = tmp->next) {
    if(strcmp(resource_name->valuestring, tmp->resource_label) == 0) { /* Found our template */
      void *model = (void *) tmp->init(resource);
      pthread_t *thread;
      add_thread(&thread);
      pthread_create(thread,NULL,thr_resource,model);
      return 0;
    }
  }
  return -1;
}

pthread_t * spawn_job(resource_model *model)
{
  mode *m = select_mode(model);
  pthread_t *thread = malloc(sizeof(pthread_t));
  pthread_create(thread, NULL,m->function, model->state);
  pthread_detach(*thread);
  return thread;
}

void * thr_resource(void *config)
{
  setup_schedule();
  struct timespec current_time = {0};
  struct timespec tmp = {0};
  resource_model *model = (resource_model *) config;
  
  rendezvous(&threads_start, &threads_ready_mutex, &threads_ready, CNT_RESOURCES, "Starting Workload Generator.");
  //Start the generator
  model->birth(model);
  
  while(has_event()) {
    event *e = next_event();
    sleep_until(e->timestamp);
#if 0
    clock_gettime(CLOCK_MONOTONIC,&current_time);
    tmp = diff(e->timestamp,&current_time);
    printf("%u\n", tmp.tv_nsec); //nanoseconds late
#endif
    e->job(e->args);
    free(e->timestamp);
    free(e);
  }
}

struct timespec * ms_from_now(double ms){
  struct timespec *result = malloc(sizeof(struct timespec));
  struct timespec now = {0};
  struct timespec to_add = {0};
  clock_gettime(CLOCK_MONOTONIC,&now);
  to_add.tv_sec = (unsigned long) floor(ms / 1000);
  to_add.tv_nsec = ((unsigned long) floor( (floatmod(ms,1000) * 1E6) ) ) % (unsigned long) 1E9;
  *result = add(now,to_add);
  return result;
}

void sleep_until(struct timespec *timestamp)
{
  struct timespec current_time = {0};
  clock_gettime(CLOCK_MONOTONIC,&current_time);
  struct timespec time_to_sleep = diff(&current_time, timestamp);
  nanosleep(&time_to_sleep, NULL);
}

struct timespec add(struct timespec x, struct timespec y) {
  struct timespec result = {0};
  unsigned long nsec_add = x.tv_nsec + y.tv_nsec;
  result.tv_sec = x.tv_sec + y.tv_sec + ( floor( nsec_add / 1E9 ) );
  result.tv_nsec = nsec_add % (unsigned long) 1E9;
  return result;
}

struct timespec diff(struct timespec *now, struct timespec *future)
{
  struct timespec to_sleep = {0};
  if ((future->tv_nsec - now->tv_nsec)<0) {
    to_sleep.tv_sec = future->tv_sec-now->tv_sec-1;
    to_sleep.tv_nsec = 1000000000+future->tv_nsec-now->tv_nsec;
  } else {
    to_sleep.tv_sec = future->tv_sec-now->tv_sec;
    to_sleep.tv_nsec = future->tv_nsec-now->tv_nsec;
  }
  return to_sleep;
}

double floatmod(double val, unsigned long mod) {
  double to_subtract = floor(val/mod);
  return val - (to_subtract * mod);
}

void * memdup(void *from, size_t size) {
  void *destination = malloc(size);
  return memcpy(destination, from, size);
}
